// AquosSimulator.cs
//
// Implements HomeUX.Components.AquosSimulator.
//
// See ..\..\Drivers\Aquos\DocFiles\AquosDriver.htm for simulator usage information.
//

using System;
using System.Globalization;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Simulates a Sharp Aquos LC-52D65U, LC-42D65U, or LC-46D65U LCD TV connected via a serial port.
/// </summary>
///
/// <remarks>
/// See <n>AquosDriver</n> documentation for <r>AquosSimulator</r> usage information.
/// </remarks>
///
public class AquosSimulator : VirtualSerialPort
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Constants
    //

    /// <summary>
    /// The number of milliseconds to wait before each reply is sent back to the serial port.
    /// Increasing this number slows down the speed of the simulation.
    /// </summary>
    const int LatencyMsec = 1000;

    /// <summary>
    /// Matches an Aquos "Power On Command Setting" command, which enables or disables the "Power
    /// Setting" command.
    /// </summary>
    static readonly Regex PowerOnCommandSettingRegex = new Regex(@"^RSPW([01])$");

    /// <summary>
    /// Matches an Aquos "Power Setting" command, which turns the TV on or off.
    /// </summary>
    static readonly Regex PowerSettingRegex = new Regex(@"^POWR([01])$");

    /// <summary>
    /// Matches an Aquos "Input Select Terminal" command, which switches the TV to a given input
    /// number (1 to 9).
    /// </summary>
    static readonly Regex InputSelectTerminalRegex = new Regex(@"^IAVD([0-9])$");

    /// <summary>
    /// Matches an Aquos "Volume" command, which sets the TV volume (0-60).
    /// </summary>
    static readonly Regex VolumeRegex = new Regex(@"^VOLM([0-9][0-9]?)$");

    /// <summary>
    /// Matches an Aquos "Mute" command, which turns mute on (1), off (2), or toggles it (0).
    /// </summary>
    static readonly Regex MuteRegex = new Regex(@"^MUTE([012])$");

    ///////////////////////////////////////////////////////////////////////////////////////////////
	// Private State
	//

	/// <summary>
	/// This object is locked before a thread accesses this object, to serialize access to the
	/// object.
	/// </summary>
	object _lock = new object();

	/// <summary>
	/// Serial port read buffer, i.e. data to send to client.
	/// </summary>
	StringBuilder _sendToClient = new StringBuilder(1000);

	/// <summary>
	/// Work queue for scheduling asynchronous operations.
	/// </summary>
	WorkQueue _workQueue;

    /// <summary>
    /// Identifies a test being performed using this simulator, or 0 if no test is being
    /// performed.
    /// </summary>
    int _test;

    /// <summary>
    /// The number of commands that have been executed.
    /// </summary>
    int _commandsExecuted;

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Public Methods
	//

	/// <summary>
	/// Creates an instance of this class.
	/// </summary>
	///
	public AquosSimulator() : this(new string[0])
	{
	}

    /// <summary>
    /// Creates an instance of this class.  Allows simulator arguments to be specified.
    /// </summary>
    ///
    /// <param name="args">Simulator arguments.  See AquosDriver.htm for details.</param>
    ///
    public AquosSimulator(string[] args)
    {
        // parse <args>
        foreach (string arg in args)
        {
            try
            {
                string[] argParts = arg.Split('=');
                if (argParts.Length != 2)
                    throw new ArgumentException();
                string argName = argParts[0];
                string argValue = argParts[1];
                switch (argName)
                {
                case "Test":
                    _test = System.Xml.XmlConvert.ToInt32(argValue);
                    break;
                default:
                    throw new ArgumentException();
                }
            }
            catch (Exception ex)
            {
                if ((ex is FormatException) || (ex is OverflowException) ||
                    (ex is ArgumentException))
                {
                    throw new VirtualSerialPortException("Invalid argument: {0}", arg);
                }
                else
                    throw;
            }
        }
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    /// <summary>
    /// Implements <n>IDisposable.Dispose</n>.
    /// </summary>
    ///
    public override void Dispose()
    {
		lock (_lock)
		{
			base.Dispose();
			if (_workQueue != null)
			{
				_workQueue.Dispose();
				_workQueue = null;
			}
		}
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// VirtualSerialPort Overrides
	//

    /// <summary>
    /// Overrides <r>VirtualSerialPort.SupportsLineBasedTextMode</r>.
    /// </summary>
    ///
	public override bool SupportsLineBasedTextMode
	{
		// this virtual serial port only supports line-based text mode
		get
		{
			return true;
		}
	}

    /// <summary>
    /// Overrides <r>VirtualSerialPort.NewLine</r>.
    /// </summary>
    ///
    public override string NewLine
	{
		get
		{
            return "\r";
		}
        set
        {
            if (value != "\r")
            {
                throw new InvalidOperationException(
					"AquosSimulator expects lines it receives to be terminated by CR");
			}
        }
	}

    /// <summary>
    /// Overrides <r>VirtualSerialPort.Open</r>.
    /// </summary>
    ///
    public override void Open()
    {
        lock (_lock)
        {
			// initialize state
			InitializeState();

            // create some fake state
			// TODO...

			// create a work queue for queueing events
			_workQueue = new WorkQueue("AquosSimulator");

			// debugging feedback
			Trace("Initialized");

            // simulate a bunch of null characters, which is common
            WriteToClient("\0\0\0\0\0\0\0\0\0\0");

#if false
            // misbehave if simulator argument Test=1 was specified
            if (_test == 1)
                _sendToClient.Append("RandomJunk\r\nFromAquosSimulator");
#endif
		}
    }

    /// <summary>
    /// Overrides <r>VirtualSerialPort.ReadExisting</r>.
    /// </summary>
    ///
    public override string ReadExisting()
	{
		lock (_lock)
		{
			string text = _sendToClient.ToString();
			_sendToClient.Length = 0;
			return text;
		}
	}

    /// <summary>
    /// Overrides <r>VirtualSerialPort.WriteLine</r>.
    /// </summary>
    /// 
    /// <param name="text">See <r>VirtualSerialPort.WriteLine</r>.</param>
    ///
    public override void WriteLine(string text)
	{
		// client sent us a command (<text>)
		lock (_lock)
		{
			try
			{
				Trace("Received command: {0}", text);
				ExecuteAquosCommand(text.Trim());
			}
			catch (AquosInvalidCommandException)
			{
                // if the command wasn't understood, behave the way the real hardware behaves,
                // i.e. send back "ERR"
				WriteToClient("ERR\r");
			}
		}
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Private Properties & Methods
	//

	/// <summary>
	/// Initializes (or reinitializes) the state of the simulated hardware.
	/// </summary>
	///
	void InitializeState()
	{
		lock (_lock)
		{
			// TODO...
		}
	}

	/// <summary>
	/// Executes an Aquos command sent from the client, after a short delay.
	/// </summary>
	///
	/// <param name="command">The Aquos command to execute, not including trailing spaces or the
	/// 	terminating carriage return.  For example, "POWR1".</param>
	///
    void ExecuteAquosCommand(string command)
    {
        _workQueue.Add(500, delegate
        {
            ExecuteAquosCommand2(command);
        });
    }
    
    /// <summary>
    /// Executes an Aquos command sent from the client.
    /// </summary>
    ///
	/// <param name="command">The Aquos command to execute, not including trailing spaces or the
	/// 	terminating carriage return.  For example, "POWR1".</param>
    ///
    void ExecuteAquosCommand2(string command)
	{
		lock (_lock)
		{
			// keep track of the number of commands that have been executed
			_commandsExecuted++;

            // interpret the command
            try
            {
                Match match;
                if ((match = PowerOnCommandSettingRegex.Match(command)).Success)
                {
                    int parameter = int.Parse(match.Groups[1].Value,
                        CultureInfo.InvariantCulture);
                    if (parameter == 1)
                        Trace("Power On Command Setting = Accepted");
                    else
                        Trace("Power On Command Setting = Rejected");
                }
                else
                if ((match = PowerSettingRegex.Match(command)).Success)
                {
                    int parameter = int.Parse(match.Groups[1].Value,
                        CultureInfo.InvariantCulture);
                    if (parameter == 1)
                        Trace("Power On");
                    else
                        Trace("Power Off");
                }
                else
                if ((match = InputSelectTerminalRegex.Match(command)).Success)
                {
                    int parameter = int.Parse(match.Groups[1].Value,
                        CultureInfo.InvariantCulture);
                    Trace("Input Select Terminal {0}", parameter);
                }
                else
                if ((match = VolumeRegex.Match(command)).Success)
                {
                    int parameter = int.Parse(match.Groups[1].Value,
                        CultureInfo.InvariantCulture);
                    Trace("Volume {0}", parameter);
                    if ((parameter < 0) || (parameter > 60))
                        Trace("Volume out of range (0-60)");
                }
                else
                if ((match = MuteRegex.Match(command)).Success)
                {
                    int parameter = int.Parse(match.Groups[1].Value,
                        CultureInfo.InvariantCulture);
                    switch (parameter)
                    {
                    case 0:
                        Trace("Mute 0 = toggle");
                        break;
                    case 1:
                        Trace("Mute 1 = on");
                        break;
                    case 2:
                        Trace("Mute 2 = off");
                        break;
                    default:
                        Trace("Mute {0} = out of range (0-2)", parameter);
                        break;
                    }
                }
                else
                {
                    // unknown command
                    throw new AquosInvalidCommandException();
                }

                // write a response to the client
                WriteToClient("OK\r");
            }
			catch (AquosInvalidCommandException)
			{
                // if the command wasn't understood, behave the way the real hardware behaves,
                // i.e. send back "ERR"
				WriteToClient("ERR\r");
			}
		}
	}

	/// <summary>
	/// Formats a string and sends it string to the client asynchronously.  The client will receive
	/// the string on the virtual serial port.
	/// </summary>
	/// 
	/// <param name="format">The error message format string.  If <pr>args</pr> is empty,
	/// 	<pr>format</pr> is used without formatting.</param>
	///
	/// <param name="args">Formatting arguments.</param>
	///
	void WriteToClient(string format, params object[] args)
	{
		lock (_lock)
		{
			WriteToClient((args.Length == 0) ? format : String.Format(format, args));
		}
	}

	/// <summary>
	/// Sends a string to the client asynchronously.  The client will receive the string on the
	/// virtual serial port.
	/// </summary>
	///
	void WriteToClient(string text)
	{
		lock (_lock)
			_sendToClient.Append(text);
		_workQueue.Add(delegate
		{
            FireDataReceived(SerialData.Chars);
		});
	}

	/// <summary>
	/// Writes a formatted string to the debug output window.
	/// </summary>
	///
	static void Trace(string format, params object[] args)
	{
		// static -- no lock required
		Util.Trace("AquosSimulator: {0}", Util.Format(format, args));
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Nested Types
	//

	/// <summary>
	/// Indicates an invalid Aquos command.
	/// </summary>
	///
	class AquosInvalidCommandException : Exception
	{
	}
}

}

